/**
 * @license
 * Copyright (C) 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import '../../../scripts/bundled-polymer.js';

import '../../../styles/shared-styles.js';
import '../../../scripts/rootElement.js';
import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';

const HOVER_CLASS = 'hovered';

/**
 * When the hovercard is positioned diagonally (bottom-left, bottom-right,
 * top-left, or top-right), we add additional (invisible) padding so that the
 * area that a user can hover over to access the hovercard is larger.
 */
const DIAGONAL_OVERFLOW = 15;

/**
 * The mixin for gr-hovercard-behavior.
 *
 * @example
 *
 * // LegacyElementMixin is still needed to support the old lifecycles
 * // TODO: Replace old life cycles with new ones.
 *
 * class YourComponent extends hovercardBehaviorMixin(
 *  LegacyElementMixin(PolymerElement)
 * ) {
 *   static get is() { return ''; }
 *   static get template() { return html``; }
 * }
 *
 * customElements.define(GrHovercard.is, GrHovercard);
 *
 * @see gr-hovercard.js
 *
 * // following annotations are required for polylint
 * @polymer
 * @mixinFunction
 */
export const hovercardBehaviorMixin = superClass => class extends superClass {
  static get properties() {
    return {
      /**
       * @type {?}
       */
      _target: Object,

      /**
       * Determines whether or not the hovercard is visible.
       *
       * @type {boolean}
       */
      _isShowing: {
        type: Boolean,
        value: false,
      },
      /**
       * The `id` of the element that the hovercard is anchored to.
       *
       * @type {string}
       */
      for: {
        type: String,
        observer: '_forChanged',
      },

      /**
       * The spacing between the top of the hovercard and the element it is
       * anchored to.
       *
       * @type {number}
       */
      offset: {
        type: Number,
        value: 14,
      },

      /**
       * Positions the hovercard to the top, right, bottom, left, bottom-left,
       * bottom-right, top-left, or top-right of its content.
       *
       * @type {string}
       */
      position: {
        type: String,
        value: 'right',
      },

      container: Object,
      /**
       * ID for the container element.
       *
       * @type {string}
       */
      containerId: {
        type: String,
        value: 'gr-hovercard-container',
      },
    };
  }

  /** @override */
  attached() {
    super.attached();
    if (!this._target) { this._target = this.target; }
    this.listen(this._target, 'mouseenter', 'show');
    this.listen(this._target, 'focus', 'show');
    this.listen(this._target, 'mouseleave', 'hide');
    this.listen(this._target, 'blur', 'hide');
    this.listen(this._target, 'click', 'hide');
  }

  /** @override */
  created() {
    super.created();
    this.addEventListener('mouseleave',
        e => this.hide(e));
  }

  /** @override */
  ready() {
    super.ready();
    // First, check to see if the container has already been created.
    this.container = Gerrit.getRootElement()
        .querySelector('#' + this.containerId);

    if (this.container) { return; }

    // If it does not exist, create and initialize the hovercard container.
    this.container = document.createElement('div');
    this.container.setAttribute('id', this.containerId);
    Gerrit.getRootElement().appendChild(this.container);
  }

  removeListeners() {
    this.unlisten(this._target, 'mouseenter', 'show');
    this.unlisten(this._target, 'focus', 'show');
    this.unlisten(this._target, 'mouseleave', 'hide');
    this.unlisten(this._target, 'blur', 'hide');
    this.unlisten(this._target, 'click', 'hide');
  }

  /**
   * Returns the target element that the hovercard is anchored to (the `id` of
   * the `for` property).
   *
   * @type {HTMLElement}
   */
  get target() {
    const parentNode = dom(this).parentNode;
    // If the parentNode is a document fragment, then we need to use the host.
    const ownerRoot = dom(this).getOwnerRoot();
    let target;
    if (this.for) {
      target = dom(ownerRoot).querySelector('#' + this.for);
    } else {
      target = parentNode.nodeType == Node.DOCUMENT_FRAGMENT_NODE ?
        ownerRoot.host :
        parentNode;
    }
    return target;
  }

  /**
   * Hides/closes the hovercard. This occurs when the user triggers the
   * `mouseleave` event on the hovercard's `target` element (as long as the
   * user is not hovering over the hovercard).
   *
   * @param {Event} e DOM Event (e.g. `mouseleave` event)
   */
  hide(e) {
    const targetRect = this._target.getBoundingClientRect();
    const x = e.clientX;
    const y = e.clientY;
    if (x > targetRect.left && x < targetRect.right && y > targetRect.top &&
        y < targetRect.bottom) {
      // Sometimes the hovercard itself obscures the mouse pointer, and
      // that generates a mouseleave event. We don't want to hide the hovercard
      // in that situation.
      return;
    }

    // If the hovercard is already hidden or the user is now hovering over the
    //  hovercard or the user is returning from the hovercard but now hovering
    //  over the target (to stop an annoying flicker effect), just return.
    if (!this._isShowing || e.toElement === this ||
        (e.fromElement === this && e.toElement === this._target)) {
      return;
    }

    // Mark that the hovercard is not visible and do not allow focusing
    this._isShowing = false;

    // Clear styles in preparation for the next time we need to show the card
    this.classList.remove(HOVER_CLASS);

    // Reset and remove the hovercard from the DOM
    this.style.cssText = '';
    this.$.container.setAttribute('tabindex', -1);

    // Remove the hovercard from the container, given that it is still a child
    // of the container.
    if (this.container.contains(this)) {
      this.container.removeChild(this);
    }
  }

  /**
   * Shows/opens the hovercard. This occurs when the user triggers the
   * `mousenter` event on the hovercard's `target` element.
   *
   * @param {Event} e DOM Event (e.g., `mouseenter` event)
   */
  show(e) {
    if (this._isShowing) {
      return;
    }

    // Mark that the hovercard is now visible
    this._isShowing = true;
    this.setAttribute('tabindex', 0);

    // Add it to the DOM and calculate its position
    this.container.appendChild(this);
    this.updatePosition();

    // Trigger the transition
    this.classList.add(HOVER_CLASS);
  }

  /**
   * Updates the hovercard's position based on the `position` attribute
   * and the current position of the `target` element.
   *
   * The hovercard is supposed to stay open if the user hovers over it.
   * To keep it open when the user moves away from the target, the bounding
   * rects of the target and hovercard must touch or overlap.
   *
   * NOTE: You do not need to directly call this method unless you need to
   * update the position of the tooltip while it is already visible (the
   * target element has moved and the tooltip is still open).
   */
  updatePosition() {
    if (!this._target) { return; }

    // Calculate the necessary measurements and positions
    const parentRect = document.documentElement.getBoundingClientRect();
    const targetRect = this._target.getBoundingClientRect();
    const thisRect = this.getBoundingClientRect();

    const targetLeft = targetRect.left - parentRect.left;
    const targetTop = targetRect.top - parentRect.top;

    let hovercardLeft;
    let hovercardTop;
    const diagonalPadding = this.offset + DIAGONAL_OVERFLOW;
    let cssText = '';

    // Find the top and left position values based on the position attribute
    // of the hovercard.
    switch (this.position) {
      case 'top':
        hovercardLeft = targetLeft + (targetRect.width - thisRect.width) / 2;
        hovercardTop = targetTop - thisRect.height - this.offset;
        cssText += `padding-bottom:${this.offset
        }px; margin-bottom:-${this.offset}px;`;
        break;
      case 'bottom':
        hovercardLeft = targetLeft + (targetRect.width - thisRect.width) / 2;
        hovercardTop = targetTop + targetRect.height + this.offset;
        cssText +=
            `padding-top:${this.offset}px; margin-top:-${this.offset}px;`;
        break;
      case 'left':
        hovercardLeft = targetLeft - thisRect.width - this.offset;
        hovercardTop = targetTop + (targetRect.height - thisRect.height) / 2;
        cssText +=
            `padding-right:${this.offset}px; margin-right:-${this.offset}px;`;
        break;
      case 'right':
        hovercardLeft = targetRect.right + this.offset;
        hovercardTop = targetTop + (targetRect.height - thisRect.height) / 2;
        cssText +=
            `padding-left:${this.offset}px; margin-left:-${this.offset}px;`;
        break;
      case 'bottom-right':
        hovercardLeft = targetRect.left + targetRect.width + this.offset;
        hovercardTop = targetRect.top + targetRect.height + this.offset;
        cssText += `padding-top:${diagonalPadding}px;`;
        cssText += `padding-left:${diagonalPadding}px;`;
        cssText += `margin-left:-${diagonalPadding}px;`;
        cssText += `margin-top:-${diagonalPadding}px;`;
        break;
      case 'bottom-left':
        hovercardLeft = targetRect.left - thisRect.width - this.offset;
        hovercardTop = targetRect.top + targetRect.height + this.offset;
        cssText += `padding-top:${diagonalPadding}px;`;
        cssText += `padding-right:${diagonalPadding}px;`;
        cssText += `margin-right:-${diagonalPadding}px;`;
        cssText += `margin-top:-${diagonalPadding}px;`;
        break;
      case 'top-left':
        hovercardLeft = targetRect.left - thisRect.width - this.offset;
        hovercardTop = targetRect.top - thisRect.height - this.offset;
        cssText += `padding-bottom:${diagonalPadding}px;`;
        cssText += `padding-right:${diagonalPadding}px;`;
        cssText += `margin-bottom:-${diagonalPadding}px;`;
        cssText += `margin-right:-${diagonalPadding}px;`;
        break;
      case 'top-right':
        hovercardLeft = targetRect.left + targetRect.width + this.offset;
        hovercardTop = targetRect.top - thisRect.height - this.offset;
        cssText += `padding-bottom:${diagonalPadding}px;`;
        cssText += `padding-left:${diagonalPadding}px;`;
        cssText += `margin-bottom:-${diagonalPadding}px;`;
        cssText += `margin-left:-${diagonalPadding}px;`;
        break;
    }

    // Prevent hovercard from appearing outside the viewport.
    // TODO(kaspern): fix hovercard appearing outside viewport on bottom and
    // right.
    if (hovercardLeft < 0) { hovercardLeft = 0; }
    if (hovercardTop < 0) { hovercardTop = 0; }
    // Set the hovercard's position
    cssText += `left:${hovercardLeft}px; top:${hovercardTop}px;`;
    this.style.cssText = cssText;
  }

  /**
   * Responds to a change in the `for` value and gets the updated `target`
   * element for the hovercard.
   *
   * @private
   */
  _forChanged() {
    this._target = this.target;
  }
};
